<?php
/**
 * Created by PhpStorm.
 * User: J.H
 * Date: 2017/4/5
 * Time: 11:05
 */

namespace lib\curl;


/**
 * Class HttpRequest
 * @package lib\curl
 */
class HttpRequest
{
    private $cookies = '';
    private $userAgent = '';
    private $timeout = 15;
    private $credentials = '';
    private $header = array();
    private $customRequest = '';
    private $options = array();


    const METHOD_GET = 'GET';
    const METHOD_POST = 'POST';

    function timeout($time = 10)
    {
        $this->timeout = $time;
    }

    function cookies($cookies = array())
    {
        if (!empty($cookies)) {
            $this->cookies = '';
            if (is_array($cookies)) {
                foreach ($cookies as $key => $val) {
                    $this->cookies .= "{$key}={$val}; ";
                }
            }
        }
    }

    function userAgent($agent = '')
    {
        if ($agent) {
            $this->userAgent = $agent;
        }
    }

    /**
     * @link http://us.php.net/manual/en/function.curl-setopt.php
     *
     * @param $option
     * @param string $value
     *
     * @return $this
     */
    function setOptions($option, $value)
    {
        $this->options[$option] = $value;
        return $this;
    }

    function setHeader(array $header)
    {
        $this->header = array_values($header);
    }

    function auth($username, $pwd)
    {
        $this->credentials = "{$username}:{$pwd}";
    }

    static function multiHttp(
        array &$data,
        $url = '',
        $method = self::METHOD_GET,
        &$ret_code = false,
        &$error_msg = array(),
        &$error_num = array()
    ) {
        return (new self())->multiRequest($data, $url, $method, $ret_code, $error_msg, $error_num);
    }

    /**
     * @param array $data
     * @param string $url
     * @param string $method
     * @param bool $ret_code
     * @param array $error_msg
     * @param array $error_num
     *
     * @return array
     */
    function multiRequest(
        array &$data, $url = '',
        $method = self::METHOD_GET,
        &$ret_code = false,
        &$error_msg = array(),
        &$error_num = array()
    ) {
        $req_list = curl_multi_init();
        $ch_list = $ri = array();
        foreach ($data as $index => $node) {

            $q_url = $url;
            if (isset($node['url'])) {
                $q_url = $node['url'];
            }

            $params = array();
            if (isset($node['params'])) {
                $params = $node['params'];
            }

            $q_method = $method;
            if (isset($node['method'])) {
                $q_method = $node['method'];
            }

            if ($q_method === self::METHOD_GET && !empty($params)) {
                $q_url = rtrim($q_url, '?') . '?' . http_build_query($params);
                $params = array();
            }
            $data[$index]['request_url'] = $q_url;

            $this->credentials = '';
            if (isset($node['auth']) && is_array($node['auth'])) {
                $user_pwd = array_values($node['auth']);
                $this->auth($user_pwd[0], $user_pwd[1]);
            }

            if (!empty($node['header'])) {
                $this->header = $node['header'];
            }

            if (!empty($node['cookies'])) {
                $this->cookies = $node['cookies'];
            }

            if (!empty($node['ua'])) {
                $this->userAgent = $node['ua'];
            }

            $ch_list[$index] = $this->getCurlObject($q_url, $params);
            $ri[(int)$ch_list[$index]] = $index;
            curl_multi_add_handle($req_list, $ch_list[$index]);
        }

        $result_data = $result_code = $result_error = $result_error_num = array();

        do {
            while (($exec = curl_multi_exec($req_list, $running)) === CURLM_CALL_MULTI_PERFORM) {
                if ($exec != CURLM_OK) {
                    break;
                }
            }

            while ($node_complete = curl_multi_info_read($req_list)) {

                $nci = (int)$node_complete['handle'];
                // 从已完成请求中获取状态、错误、内容
                if (false !== $ret_code) {
                    $result_code[$ri[$nci]] = curl_getinfo($node_complete['handle'], CURLINFO_HTTP_CODE);
                }
                $result_data[$ri[$nci]] = $data[$ri[$nci]]['result_content'] = curl_multi_getcontent($node_complete['handle']);
                if ($enum = curl_errno($node_complete['handle']) > 0) {
                    $result_error_num[$ri[$nci]] = $enum;
                    $result_error[$ri[$nci]] = curl_error($node_complete['handle']);
                    $result_data[$ri[$nci]] = $data[$ri[$nci]]['result_content'] = false;
                }
                curl_close($node_complete['handle']);
                curl_multi_remove_handle($req_list, $node_complete['handle']);
            }

            if ($running) {
                $rel = curl_multi_select($req_list, 0.5);
                if ($rel == -1) {
                    usleep(1000);
                }
            }

            if (false == $running) {
                break;
            }
        } while (true);

        curl_multi_close($req_list);
        $ret_code = $result_code;
        $error_msg = $result_error;
        $error_num = $result_error_num;
        return $result_data;
    }

    /**
     * 发送一个curl请求
     *
     * @param string $url
     * @param array|string $params
     * @param string $method
     * @param bool $error_code
     * @param string $error_msg
     * @param int $http_code
     *
     * @return bool|mixed
     */
    function request(
        $url, $params = array(), $method = self::METHOD_GET, &$error_code = false, &$error_msg = '',
        &$http_code = 0
    ) {
        if ($method === self::METHOD_GET && !empty($params)) {
            $url = rtrim($url, '?') . '?' . http_build_query($params);
            $params = array();
        }
        $ch = $this->getCurlObject($url, $params);
        $result = curl_exec($ch);
        if (false !== $error_code) {
            $error_code = curl_errno($ch);
        }
        if (curl_errno($ch) > 0) {
            $error_msg = curl_error($ch);
            $result = false;
        }
        if (0 !== $http_code) {
            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        }
        curl_close($ch);
        return $result;
    }

    /**
     * GET请求
     * @see request()
     *
     * @param string $url
     * @param array|string $params
     * @param bool $error_code
     * @param string $error_msg
     *
     * @return bool|mixed
     */
    static function get($url, $params = array(), &$error_code = false, &$error_msg = '')
    {
        return (new self())->request($url, $params, self::METHOD_GET, $error_code, $error_msg);
    }

    /**
     * POST请求
     * @see request()
     *
     * @param string $url
     * @param array|string $params
     * @param bool $error_code
     * @param string $error_msg
     *
     * @return bool|mixed
     */
    static function post($url, $params, &$error_code = false, &$error_msg = '')
    {
        return (new self())->request($url, $params, self::METHOD_POST, $error_code, $error_msg);
    }

    /**
     * PUT请求
     * @see request()
     *
     * @param string $url
     * @param array|string $params
     * @param bool $error_code
     * @param string $error_msg
     *
     * @return bool|mixed
     */
    static function put($url, $params, &$error_code = false, &$error_msg = '')
    {
        return (new self())->setReqMethod('PUT')->request($url, $params, 'put', $error_code, $error_msg);
    }

    /**
     * DELETE请求
     * @see request()
     *
     * @param string $url
     * @param array|string $params
     * @param bool $error_code
     * @param string $error_msg
     *
     * @return bool|mixed
     */
    static function delete($url, $params, &$error_code = false, &$error_msg = '')
    {
        return (new self())->setReqMethod('DELETE')->request($url, $params, 'delete', $error_code, $error_msg);
    }

    private function setReqMethod($method = '')
    {
        $this->customRequest = $method;
        $this->header[] = "X-HTTP-Method-Override: {$method}";
        return $this;
    }

    /**
     * @param string $url
     * @param array $params
     * @param int $connect_timeout
     *
     * @return bool
     */
    static function asyncGet($url, array $params = array(), $connect_timeout = 1)
    {
        return self::asyncRequest($url, $params, self::METHOD_GET, $connect_timeout);
    }

    /**
     * @param string $url
     * @param array $params
     * @param int $connect_timeout
     *
     * @return bool
     */
    static function asyncPost($url, array $params = array(), $connect_timeout = 1)
    {
        return self::asyncRequest($url, $params, self::METHOD_POST, $connect_timeout);
    }

    /**
     * @param string $url
     * @param array $params
     * @param string $method
     * @param int $connect_timeout
     *
     * @return bool
     * @throws \Exception
     */
    static function asyncRequest($url, array $params = array(), $method = self::METHOD_GET, $connect_timeout = 1)
    {

        $url_info = parse_url($url);
        if (!isset($url_info['host'])) {
            throw new \Exception('invalid url');
        }
        $hostname = $url_info['host'];
        if (isset($url_info['port'])) {
            $port = $url_info['port'];
        }
        if (!isset($port) && isset($url_info['scheme'])) {
            $port = strtolower($url_info['scheme']) === 'https' ? 443 : 80;
        }

        $path = isset($url_info['path']) ? $url_info['path'] : '/';

        $params = http_build_query($params);
        $params_length = strlen($params);
        $query = '?';
        if (isset($url_info['query'])) {
            $query .= rtrim($url_info['query'], '&');
        }
        if ($method === self::METHOD_GET && $params_length > 0) {
            $query .= ('?' === $query) ? $params : '&' . $params;
        }
        $path .= $query;

        $fp = fsockopen($hostname, $port, $errno, $errstr, $connect_timeout);

        if ($fp) {
            $headers = array(
                "{$method} {$path} HTTP/1.1",
                "Host: {$hostname}",
                "Content-type: application/x-www-form-urlencoded",
                "Connection: Close",
                "User-Agent: AsyncRequest"
            );
            if ($method === self::METHOD_POST) {
                $headers[] = "Content-Length: {$params_length}\r\n";
                $headers[] = $params;
            }
            if (fwrite($fp, implode("\r\n", $headers)) === false || fclose($fp) === false) {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * 建立CURL RESOURCE
     *
     * @param $url
     * @param array|string $postData
     *
     * @return resource
     */
    private function getCurlObject($url, $postData = array())
    {
        $options = array();
        $url = trim($url);
        $options[CURLOPT_URL] = $url;
        $options[CURLOPT_TIMEOUT] = $this->timeout;
        $options[CURLOPT_RETURNTRANSFER] = true;
        if ($this->userAgent) {
            $options[CURLOPT_USERAGENT] = $this->userAgent;
        }
        if ($this->cookies) {
            $options[CURLOPT_COOKIE] = $this->cookies;
        }
        if ($this->credentials) {
            $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
            $options[CURLOPT_USERPWD] = $this->credentials;
        }
        if (!empty($this->header) && is_array($this->header)) {
            $options[CURLOPT_HTTPHEADER] = array_values($this->header);
        }
        if ($this->customRequest) {
            $options[CURLOPT_CUSTOMREQUEST] = $this->customRequest;
        }
        if (!empty($postData)) {
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = $postData;
        }
        if (stripos($url, 'https') === 0) {
            $options[CURLOPT_SSL_VERIFYPEER] = false;
            $options[CURLOPT_SSL_VERIFYHOST] = false;
        }
        foreach ($this->options as $option => $val) {
            $options[$option] = $val;
        }
        $ch = curl_init();
        curl_setopt_array($ch, $options);

        return $ch;
    }

    static function buildGetUrl($url, array $params)
    {
        if (empty($params)) {
            return $url;
        }
        return rtrim($url, '?') . '?' . http_build_query($params);
    }
}